Skip to content

F# hot reload: Edit-and-Continue delta emission behind --test:HotReloadDeltas#19941

Open
NatElkins wants to merge 385 commits into
dotnet:mainfrom
NatElkins:hot-reload-v2
Open

F# hot reload: Edit-and-Continue delta emission behind --test:HotReloadDeltas#19941
NatElkins wants to merge 385 commits into
dotnet:mainfrom
NatElkins:hot-reload-v2

Conversation

@NatElkins

@NatElkins NatElkins commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

This draft PR presents the complete F# hot reload implementation as a single branch against current main: dotnet watch (with a companion dotnet-watch change, linked below) patches running F# processes in place via the standard EnC pipeline (MetadataUpdater.ApplyUpdate) — the same runtime contract C# uses. Unsupported edits degrade to the rebuild-and-restart flow watch uses today, so a limited scope still behaves as a complete feature.

It is opened as a single draft per discussion with @T-Gro: one branch to review, build, and launch testing from. A decomposition into independently shippable PRs is laid out below and can begin whenever review reaches that stage. Earlier discussion: #11636.

Try it (30–45 min, two clones, copy-paste steps)

docs/hot-reload-quickstart.md walks from git clone to editing a running F# app — including adding a List.map (fun s -> s.ToUpper()) to a live method and watching it apply in place, state preserved. Short version: build this branch, build NatElkins/sdk fsharp-hotreload-watch-v2 (a complete .NET CLI with an F#-aware dotnet watch), copy the freshly built FSharp.Compiler.Service.dll into the SDK layout, add <OtherFlags>$(OtherFlags) --test:HotReloadDeltas</OtherFlags> to a console app, and dotnet watch run.

What works

  • Method body edits, including bodies containing closures, async, resume-point-stable task, and generics
  • Resumable state-machine shape edits (behind --test:HotReloadClassStateMachines): adding or removing a let!/do! in task and backgroundTask, which emits class-form (reference-type) state machines so the change is an AddInstanceFieldToExistingType plus a method update, matching C#. taskSeq and other resumable CEs share the same lowering path. Off by default; flag-off codegen is byte-identical.
  • Lambdas added / edited / removed across generations (new closure classes synthesized into the running process)
  • Member additions: methods, module functions and values, static/instance fields, properties, [<CLIEvent>] events
  • New type definitions: classes, records, unions, structs, modules, enums, interfaces, delegates, units of measure
  • Attribute add/change/remove on existing members; parameter renames
  • Multi-project: one watch session, per-project baselines, interleaved edits across an app and its referenced libraries
  • Line-shift edits (comment/whitespace above code) become pure sequence-point line updates — no delta, no restart
  • Rude edits (signature changes, captured-variable rename/type/scope changes, plus the F#-specific inline-annotation change) degrade to the standard rebuild-and-restart flow with a precise diagnostic. Adding or removing a mid-sequence let!/do! is rude under the default struct state machines, but becomes a supported edit (an AddInstanceFieldToExistingType plus a method update, matching C#) under --test:HotReloadClassStateMachines

Isolation and disabling

The feature is designed so that a compile without the flag is indistinguishable from main, and so that the whole feature can be turned off at one point if something goes wrong:

  • Off by default. Everything is behind --test:HotReloadDeltas (requires --debug+, incompatible with --optimize+), with class-form resumable state machines a further opt-in behind --test:HotReloadClassStateMachines. Both are off by default; no MSBuild property or SDK behavior changes in this repo.
  • Flag-off output is byte-identical to main, pinned by the EmittedIL suite (1212 baseline tests) and by dedicated determinism tests (DLL+PDB byte-equality across recompiles and across graph/sequential checking modes).
  • Flag-off cost is zero beyond cheap checks. A dedicated audit pass removed all unconditional work from the flag-off path: no reflection, no eager metadata snapshots, no per-name or per-closure side-channel probes, no extended lifetime of the optimized typed tree, and FSharpProjectSnapshot.fs is byte-identical to main (tracked-input staleness lives in the hot reload session layer instead).
  • One seam. The compiler driver integration is a single ICompilerEmitHook interface with a no-op default. Guard scripts under tests/scripts/ (run by the verification gate) enforce that only the intended files consume the hook and that the IlxGen name-generation path and fsi surface stay on their pinned shapes.
  • Disabling: drop the flag (compiler side); the companion dotnet-watch side additionally has a one-variable kill switch (DOTNET_WATCH_FSHARP_HOTRELOAD=0) that restores stock restart-on-edit behavior.

Architecture

  • Sessions are an explicit entity: FSharpChecker.CreateHotReloadSession returns a FSharpHotReloadSession holding per-project committed snapshots + emit baselines — the DebuggingSession/CommittedSolution shape from Roslyn, built on FSharpProjectSnapshot (the snapshot contract, not the experimental workspace surface), so it composes with the FSharpWorkspace direction without depending on it. Solution-wide commit/discard semantics, runtime-capability updates, active-statement intake.
  • A typed-tree semantic diff classifies every edit (Roslyn's SemanticEdit/RudeEdit model), gated on the runtime's advertised EnC capabilities (AddMethodToExistingType, NewTypeDefinition, GenericUpdateMethod, …). Anything unclassifiable fails closed.
  • Delta emission produces the standard EnC triplet (metadata/IL/PDB deltas with EncLog/EncMap), validated against recorded Roslyn EmitDifference reference deltas, with mdv, and against CoreCLR ApplyUpdate in runtime tests.
  • Closure identity is solved the way Roslyn solves it, adapted to F#'s lowering: lambdas get stable identity from a typed-tree occurrence model (ordinal chains + LCS alignment rather than syntax offsets), persisted in Roslyn's exact portable-PDB EnC CDI blob formats (the encoder round-trips Roslyn's own blobs byte-identically), with deterministic occurrence-derived closure-class names so any process can reconstruct identity from the PDB alone.

Design documentation (rendered, on this branch)

Doc What it covers
hot-reload-architecture.md Start here. The entity model: FSharpHotReloadSession, per-project committed snapshots + baselines on FSharpProjectSnapshot, the FSharpWorkspace relationship, determinism pins
hot-reload-closure-mapping.md The closure problem and its solution: lambda occurrence model, Roslyn-format EnC CDI PDB blobs, occurrence-derived deterministic closure naming, cross-process reconstruction; state-machine handling
hot-reload-member-additions.md Recorded Roslyn EmitDifference reference templates (EncLog/EncMap shapes per edit kind) and the F# emission matrix incl. every intentional fail-closed case
hot-reload-capabilities.md Runtime capability negotiation (Roslyn EditAndContinueCapabilities parity) and per-capability gating
hot-reload-active-statements.md The debugger-contract mirror: active statements, sequence-point updates, remapping; host wiring deferred

Scale and review shape

~118 commits, 159 files, ~60k insertions, of which ~34k are tests and ~2.4k docs. The src/ changes are predominantly new self-contained modules (IlxDeltaEmitter.fs, TypedTreeDiff.fs, HotReloadBaseline.fs, the AbstractIL EnC readers, the delta writer stack). Pre-existing files carry hook callouts plus one behavior-neutral refactor (ilwrite.fs MetadataTable record→class, exposing a baseline-row access seam for the delta writer); total deletions across the branch are 241 lines.

Proposed path to merging in pieces

The fail-closed design means scope can grow capability by capability — each unsupported case is already a rude edit with a diagnostic, so every intermediate state is a complete, working feature. The natural sequence:

  1. Behavior-neutral AbstractIL/ilwrite foundations (no feature, byte-identity evidence) — already staged separately as a 3-commit branch (refactor: AbstractIL EnC foundations (hot reload stack 1/n) NatElkins/fsharp#2)
  2. Session entity + typed-tree classification, with every edit classified rude — end-to-end complete at minimal scope (watch restarts on every edit, but through the proper pipeline). This is the PR where the architecture gets decided with reviewers.
  3. Method-body-only deltas (the core)
  4. Closure mapping + deterministic naming (the one chunk that touches IlxGen/name-generation paths — reviewed on its own)
  5. Member/field/type additions, generics, state machines — each already individually capability-gated
  6. Active statements / sequence-point updates

Evidence

Known limitations / future work

Companion PRs

Refresh status (2026-07-17)

  • Refreshed against dotnet/fsharp main at 5928e91; the current reviewed head is 9d2e6e3.
  • A dedicated review pass completed for this PR, its findings were fixed in the lowest owning slice, and the PR has no unresolved review threads.
  • The downstream session, in-process compiler, umbrella, and SDK branches were restacked after the fixes, so this slice remains part of the decomposed review train.
  • The complete compiler stack passed the 11-step hot reload verifier, 456 service tests, 243 component tests with 2 expected skips, and 1411 EmittedIL tests with 3 expected skips. Replacement CI passed on this exact head.

Refresh status (2026-07-22)

  • Current head: aaaf715, refreshed against dotnet/fsharp main 69fca7f.
  • Stack position: complete umbrella containing the reviewed wave-1 foundations, baseline reader, delta emitter, session/API, and experimental in-process slice. The focused PRs still target main and must be refreshed normally as earlier slices merge.
  • Every applicable July 21 automated finding is fixed in its lowest owning slice and propagated here. All review threads across the nine open F# PRs are resolved.
  • Exact-head local verifier: all 11 steps passed, including 468 service tests, 246 component passes with 2 documented manual-host skips, 29 metadata-parity tests, both smoke modes, and direct two-generation runtime apply. The flag-off EmittedIL gate passed 1411 tests with 3 known skips before the final flag-on-only corrections.
  • Exact-head Azure compiler CI passed all 46 jobs. The base-branch check_release_notes workflow still fails before analysis because pull_request_target refuses fork checkout.

Refresh status (2026-07-24)

  • Current head: 2c8d3a7081, with dotnet/fsharp main 1dc395ad34 and the exact refreshed focused stack merged in dependency order.
  • Every actionable review finding is fixed or explicitly answered, including the Windows loaded-file report and the added-await parity follow-up. Every review thread is resolved.
  • The full verifier passes on this exact head: solution build, all structural guards, 472 service tests, 248 component tests with 2 intentional manual-host skips, both smoke modes, and direct multi-delta runtime apply. Repository-wide Fantomas and diagnostic sorting checks pass.
  • Release notes are deduplicated and F# hot reload: Edit-and-Continue delta emission behind --test:HotReloadDeltas #19941 appears exactly once. Exact-head Azure build 1525313 passed all 46 jobs; the base-branch fork-checkout failure remains the documented Secure release-note checks for fork pull requests #20081 exception.

@github-actions

github-actions Bot commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

✅ No release notes required

@NatElkins NatElkins mentioned this pull request Jun 12, 2026
@xperiandri

Copy link
Copy Markdown
Contributor

Looks like it requires rebase/conficts fix

Comment thread src/Compiler/Driver/fsc.fs Outdated
Comment thread src/Compiler/Generated/CompilerGeneratedNameMapState.fs Outdated
Comment thread src/Compiler/CodeGen/DeltaMetadataTables.fs Outdated
Comment thread src/Compiler/CodeGen/FSharpDeltaMetadataWriter.fs Outdated
Comment thread src/Compiler/TypedTree/CompilerGlobalState.fs
Comment thread src/Compiler/Generated/CompilerGeneratedNameMapState.fs Outdated
Comment thread src/Compiler/HotReload/ActiveStatements.fs
Comment thread src/Compiler/HotReload/HotReloadState.fs Outdated
Comment thread tests/FSharp.Compiler.Service.Tests/HotReload/ThreadSafetyTests.fs
Comment thread src/Compiler/TypedTree/TypedTreeDiff.fs
Comment thread src/Compiler/CodeGen/HotReloadPdb.fs
Comment thread src/Compiler/Service/service.fs Outdated
@WizMe-M

WizMe-M commented Jun 21, 2026

Copy link
Copy Markdown

Hi, I gave a try to hot-reload demo. I followed steps 1-5 and failed. What am I doing wrong?

First of all step 4 says:

Run it:
dotnet watch run --non-interactive
You should see ⌚ F# hot reload session prestarted, then the counter ticking once a second.
But my output was:

Missing `F# hot reload session prestarted`
HotReloadDemo> dotnet watch run --non-interactive
dotnet watch 🔥 Hot reload enabled. For a list of supported edits, see https://aka.ms/dotnet/hot-reload.
dotnet watch 💡 Press Ctrl+R to restart.
Restore complete (0,4s)
    info NETSDK1057: You are using a preview version of .NET. See: https://aka.ms/dotnet-support-policy
  HotReloadDemo net11.0 succeeded (2,7s) → bin\Debug\net11.0\HotReloadDemo.dll

Build succeeded in 3,7s
dotnet watch ⌚ Loading projects ...
dotnet watch ⌚ Loaded 1 project(s) in 0,4s.
dotnet watch ⌚ Waiting for changes
hello (count: 1) <--------------------------- missing 'F# hot reload session prestarted'
Looks like SDK/F#-compiler with hot-reload wasn't built.

After that I've tried to modify Program.fs and got expected result (TLDR: not works):

TLDR: not works
HotReloadDemo> dotnet watch run --non-interactive
dotnet watch 🔥 Hot reload enabled. For a list of supported edits, see https://aka.ms/dotnet/hot-reload.
dotnet watch 💡 Press Ctrl+R to restart.
Restore complete (0,4s)
    info NETSDK1057: You are using a preview version of .NET. See: https://aka.ms/dotnet-support-policy
  HotReloadDemo net11.0 succeeded (2,7s) → bin\Debug\net11.0\HotReloadDemo.dll

Build succeeded in 3,7s
dotnet watch ⌚ Loading projects ...
dotnet watch ⌚ Loaded 1 project(s) in 0,4s.
dotnet watch ⌚ Waiting for changes
hello (count: 1)
...
hello (count: 32)
dotnet watch ⌚ File updated: .\Program.fs
hello (count: 33)
...
hello (count: 56)
dotnet watch ⌚ File updated: .\Program.fs
hello (count: 57)
...
hello (count: 68)
dotnet watch 🔄 Restart requested. <------------------- ctrl+R
hello (count: 69)
hello (count: 70)
hello (count: 71)
dotnet watch ⌚ [HotReloadDemo (net11.0)] Exited
dotnet watch 🔄 Restarting.
Restore complete (0,5s)
    info NETSDK1057: You are using a preview version of .NET. See: https://aka.ms/dotnet-support-policy
  HotReloadDemo net11.0 succeeded (0,2s) → bin\Debug\net11.0\HotReloadDemo.dll

Build succeeded in 1,3s
dotnet watch ⌚ Loading projects ...
dotnet watch ⌚ Loaded 1 project(s) in 0,1s.
dotnet watch ⌚ Waiting for changes
HOT-RELOAD works (count: 1)   <------------------- only after ctrl+R
HOT-RELOAD works (count: 2)

Info:

  • OS: Windows 11 Pro (ran all steps from Powershell)
  • All $env variables were set correcly following manual
  • F#-compiler was built with no errors and warnings
  • SDK was built on third try with no errors and warnings
dotnet --info
HotReloadDemo> dotnet --info
.NET SDK:
 Version:           11.0.100-dev
 Commit:            ab1a0a0dbb
 Workload version:  11.0.100-manifests.844758e0
 MSBuild version:   18.8.0-preview-26277-111+6ca055abb

Runtime Environment:
 OS Name:     Windows
 OS Version:  10.0.26200
 OS Platform: Windows
 RID:         win-x64
 Base Path:   ~\sdk-hotreload\artifacts\bin\redist\Debug\dotnet\sdk\11.0.100-dev\

.NET workloads installed:
 [maui-windows]
   Installation Source: VS 17.14.37328.6
   Manifest Version:    11.0.0-preview.1.26102.3/11.0.100-preview.1
   Manifest Path:       ~\sdk-hotreload\artifacts\bin\redist\Debug\dotnet\sdk-manifests\11.0.100-preview.1\microsoft.net.sdk.maui\11.0.0-preview.1.26102.3\WorkloadManifest.json
   Install Type:        FileBased

 [maccatalyst]
   Installation Source: VS 17.14.37328.6
   Manifest Version:    26.2.11310-net11-p1/11.0.100-preview.1
   Manifest Path:       ~\sdk-hotreload\artifacts\bin\redist\Debug\dotnet\sdk-manifests\11.0.100-preview.1\microsoft.net.sdk.maccatalyst\26.2.11310-net11-p1\WorkloadManifest.json
   Install Type:        FileBased

 [ios]
   Installation Source: VS 17.14.37328.6
   Manifest Version:    26.2.11310-net11-p1/11.0.100-preview.1
   Manifest Path:       ~\sdk-hotreload\artifacts\bin\redist\Debug\dotnet\sdk-manifests\11.0.100-preview.1\microsoft.net.sdk.ios\26.2.11310-net11-p1\WorkloadManifest.json
   Install Type:        FileBased

 [android]
   Installation Source: VS 17.14.37328.6
   Manifest Version:    36.1.99-preview.1.119/11.0.100-preview.1
   Manifest Path:       ~\sdk-hotreload\artifacts\bin\redist\Debug\dotnet\sdk-manifests\11.0.100-preview.1\microsoft.net.sdk.android\36.1.99-preview.1.119\WorkloadManifest.json
   Install Type:        FileBased

Configured to use workload sets when installing new manifests.
No workload sets are installed. Run "dotnet workload restore" to install a workload set.

Host:
  Version:      11.0.0-preview.6.26277.111
  Architecture: x64
  Commit:       6ca055abbe

.NET SDKs installed:
  11.0.100-dev [~\sdk-hotreload\artifacts\bin\redist\Debug\dotnet\sdk]

.NET runtimes installed:
  Microsoft.AspNetCore.App 11.0.0-preview.5.26227.104 [~\sdk-hotreload\artifacts\bin\redist\Debug\dotnet\shared\Microsoft.AspNetCore.App]
  Microsoft.AspNetCore.App 11.0.0-preview.6.26277.111 [~\sdk-hotreload\artifacts\bin\redist\Debug\dotnet\shared\Microsoft.AspNetCore.App]
  Microsoft.NETCore.App 6.0.36 [~\sdk-hotreload\artifacts\bin\redist\Debug\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.NETCore.App 7.0.20 [~\sdk-hotreload\artifacts\bin\redist\Debug\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.NETCore.App 8.0.28 [~\sdk-hotreload\artifacts\bin\redist\Debug\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.NETCore.App 9.0.17 [~\sdk-hotreload\artifacts\bin\redist\Debug\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.NETCore.App 10.0.9 [~\sdk-hotreload\artifacts\bin\redist\Debug\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.NETCore.App 11.0.0-preview.5.26227.104 [~\sdk-hotreload\artifacts\bin\redist\Debug\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.NETCore.App 11.0.0-preview.6.26277.111 [~\sdk-hotreload\artifacts\bin\redist\Debug\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.WindowsDesktop.App 11.0.0-preview.5.26227.104 [~\sdk-hotreload\artifacts\bin\redist\Debug\dotnet\shared\Microsoft.WindowsDesktop.App]
  Microsoft.WindowsDesktop.App 11.0.0-preview.6.26277.111 [~\sdk-hotreload\artifacts\bin\redist\Debug\dotnet\shared\Microsoft.WindowsDesktop.App]

Other architectures found:
  x86   [C:\Program Files (x86)\dotnet]
    registered at [HKLM\SOFTWARE\dotnet\Setup\InstalledVersions\x86\InstallLocation]

Environment variables:
  DOTNET_MSBUILD_SDK_RESOLVER_CLI_DIR      [~/sdk-hotreload/artifacts/bin/redist/Debug/dotnet]
  DOTNET_MULTILEVEL_LOOKUP                 [0]
  DOTNET_ROLL_FORWARD_TO_PRERELEASE        [1]
  DOTNET_ROOT                              [~/sdk-hotreload/artifacts/bin/redist/Debug/dotnet]

global.json file:
  Not found

Learn more:
  https://aka.ms/dotnet/info

Download .NET:
  https://aka.ms/dotnet/download

@ijklam

ijklam commented Jun 21, 2026

Copy link
Copy Markdown
Contributor

After spending one or two hours debugging this, I found why this cannot work on Windows. The issue is that on the Windows, when the program is running, the program file will be locked. As a result, the "dotnet build" executed by the dotnet-watch's FSharpHotReloadService fails, cause the hot reload cannot work.

I finally have managed to make it work, done some simple test on it, and found some problems:

  1. It seems working only when editing the content in the method body.
type Greeter() =
    let mutable count = 0

    member _.Message() =
        count <- count + 1  // Modifying this number is OK
        task {
           do! Task.Delay 2000 // Modifying this number does not take effect until restart
        }
        |> _.GetAwaiter().GetResult()
        sprintf "hello (count: %d)" count  // Modifying this string is OK

let greeter = Greeter()

while true do
    printfn "%s" (greeter.Message())   // Modifying this string doesn't take effect, until restart the program
    System.Threading.Thread.Sleep(1000)   // Changing this 1000 to 2000 doesn't take effect, until restart the program
  1. Simply adding a do! ... in a task block will need to restart the program.
    member _.Message() =
        count <- count + 1
        task {
          do! Task.Delay 2000
          do! Task.Delay 2000  // add this line
        }
  1. Adding or removing lines around a task needs restart program.
    member _.Message() =
        count <- count + 1  // remove this line
        task {
          do! Task.Delay 2000
        }

NatElkins added a commit to NatElkins/sdk that referenced this pull request Jun 22, 2026
The per-edit dotnet build refreshed the bin output the running process has
loaded. Windows locks that file against writes while the app runs, so the
build failed on every edit and the change fell back to a full restart instead
of applying in place.

Build the Compile target only (the obj intermediate assembly fsc writes) and
point the hot reload session's baseline and emit reads at that intermediate
assembly via FSharpProjectInfo.IntermediateAssemblyPath. The running process
never loads that file, so nothing is locked, and the bin output is left at
generation 0 until the next real restart. macOS and Linux already tolerated
overwriting a mapped file; this lines all three platforms up on the same path.

Reported on dotnet/fsharp#19941.
@NatElkins

NatElkins commented Jun 22, 2026

Copy link
Copy Markdown
Contributor Author

@ijklam got it right. The bridge ran a full dotnet build on every edit, and the last step of that build copies the assembly into bin, which is the file the running app has loaded. Windows keeps that locked while the process is alive, so the copy failed, the build failed with it, and the edit fell back to a restart. macOS and Linux let you replace a file that's mapped into a live process, so it only bit on Windows.

The fix builds just the Compile target instead, so fsc refreshes the intermediate assembly under obj and nothing writes to the loaded bin copy. The session reads the delta from that intermediate assembly now. bin stays put until the next real restart, and all three platforms take the same path.

It's on fsharp-hotreload-watch-v2 (fcf9b49). Only the dotnet-watch side changed, so you just need to pull that branch and rebuild the sdk clone, no need to rebuild the compiler or recopy the FCS dll. I don't have a Windows machine handy at the moment, so if one of you can confirm it works there now that would help a lot.

@WizMe-M the missing F# hot reload session prestarted line was a red herring. It only prints with DOTNET_WATCH_TRACE_FSHARP_HOTRELOAD=1 set, so not seeing it didn't mean anything was broken. I've reworded the quickstart to say that, and to point at the counter instead (it keeps climbing across edits when reloads land, and resets to 1 on a restart).

On the other observations: editing the top level while loop, or adding a do! to a task block, or moving lines around one, are mostly the expected limits rather than the Windows bug. The running loop is the active frame, so it can't be swapped while it's sitting on the stack, and inserting an await into a state machine is a rude edit in C# too. The count and string edits applying in place is exactly the case that should work. If the line-shift-around-a-task case still misbehaves after the rebuild, ping me, that one is worth a second look.

EDIT: Correcting myself on the struck-out line. @ijklam is right, C# hot reload does support adding an await/do! into a state machine, I read the EnC rules wrong. Roslyn treats it as a normal method update (gated on the runtime's AddInstanceFieldToExistingType capability), and only makes it a rude edit when the method is suspended at an active statement at the moment you save. So adding a do! to a task is a genuine parity gap on the F# side today, not an expected limit. I'm going to look into closing it.

@ijklam

ijklam commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

I think "adding or removing lines before a task block needs a restart" is something worth to be solved, if we hope the feature can help us in the routine coding, as there may be many task in a file, to make the program running asynchronously.

inserting an await into a state machine is a rude edit in C# too

By the way, C# hot reload supports this already.

@NatElkins NatElkins changed the title F# hot reload: Edit-and-Continue delta emission behind --enable:hotreloaddeltas F# hot reload: Edit-and-Continue delta emission behind --test:HotReloadDeltas Jun 23, 2026
NatElkins added a commit to NatElkins/fsharp that referenced this pull request Jun 23, 2026
… identity

The resumable-code/trait shape digest rendered builder-call type
instantiations through TType.ToString() (tyToString), whose depth-limited
LimitedToString(4) collapses a deeply-solved typar to the literal "True".
That made the digest non-injective: a `task` whose return type is `int`
both sides could render Bind<int,int,int> before an edit and
Bind<int,True,True> after, producing a false StateMachineShapeChange
(FSHRDL013) rude edit.

Render the digest through tryTypeIdentityFromTType -- the same injective
runtime-identity encoder the capture path already stores -- threading the
method's typar->ordinal map into collectLoweredShapeInfo and
traitConstraintShapeDigest, with a structured formatter and a display-string
fallback only for types the encoder cannot represent. Update the architecture
guard for the new traitConstraintShapeDigest signature.

Addresses review feedback on dotnet#19941.
NatElkins added a commit to NatElkins/fsharp that referenced this pull request Jun 23, 2026
FSharpEditAndContinueLanguageService.UpdateActiveStatements had no callers:
the live path is SetActiveStatements (FSharpHotReloadSession.SetActiveStatements
-> SetSessionActiveStatements -> editAndContinueService.SetActiveStatements).
Remove the dead member and its now-orphaned HotReloadSessionStore.UpdateActiveStatements
backing, whose only caller was the dead forwarder.

Addresses review feedback on dotnet#19941.
Comment thread src/Compiler/HotReload/EditAndContinueLanguageService.fs Outdated
Comment thread src/Compiler/HotReload/RudeEditDiagnostics.fs
Comment thread src/Compiler/HotReload/EditAndContinueLanguageService.fs Outdated
Comment thread src/Compiler/FSComp.txt
Comment thread src/Compiler/Driver/CompilerConfig.fs
NatElkins added a commit to NatElkins/fsharp that referenced this pull request Jun 23, 2026
…error channel

Rude-edit reasons were flattened to a single string at the point of failure
(UnsupportedEdit of string), discarding the per-edit Id, Severity and symbol
before they reached the public API and the dotnet-watch bridge. Carry them
structurally instead:

* RudeEditDiagnostic gains a Severity (FSharpDiagnosticSeverity). Every kind is
  Error today; severityOf is the single place to introduce a non-blocking
  Warning later (e.g. a "might not take effect" edit).
* HotReloadError.UnsupportedEdit and the public FSharpHotReloadError.UnsupportedEdit
  now carry a list of structured diagnostics. A new public FSharpHotReloadRudeEdit
  record exposes Id, Severity, Message and SymbolName.
* The active-statement, deleted-symbol, mapping-error and emit-exception paths
  wrap their ad-hoc reason via RudeEditDiagnostics.unsupported so every error
  still carries an id.

The id namespace is unchanged and still owned solely by
RudeEditDiagnostics.diagnosticId, so the channel itself is id-agnostic.

Addresses review feedback on dotnet#19941.
NatElkins added a commit to NatElkins/fsharp that referenced this pull request Jun 23, 2026
…he swap point

Document, at RudeEditDiagnostics.diagnosticId (the single place the rude-edit id
namespace is decided), why F# keeps its own FSHRDL* codes rather than emitting
Roslyn's ENC* codes, and note the closest ENC analogs. Kept as a separate commit
from the channel work so aligning with the ENC codes later is an isolated change.

Addresses review feedback on dotnet#19941.
NatElkins added a commit to NatElkins/sdk that referenced this pull request Jun 23, 2026
…t-watch

The F# bridge collapsed the rude-edit reason to a record/DU ToString() dump and
logged it at Debug only, so an edit that forces a restart gave the user no reason.
Now that FSharpHotReloadError.UnsupportedEdit carries a structured rude-edit list
(Id + Severity + Message), read it via reflection (TryFormatRudeEdits) into a clean
"{Id}: {Message}" reason and report it as a warning instead of at Debug. The
extraction is defensive: any shape mismatch falls back to ToString(), so the bridge
stays correct against older FCS builds (where UnsupportedEdit still carries a string).

Pairs with the FCS-side structured diagnostics channel on dotnet/fsharp#19941.
Addresses review feedback on #1.
Comment thread src/Compiler/HotReload/EditAndContinueLanguageService.fs
@NatElkins

Copy link
Copy Markdown
Contributor Author

@WizMe-M @ijklam I went back through this feedback while refreshing the stack.

The Windows file-lock problem is fixed in the SDK PR. The F# bridge now builds only the Compile target and reads the intermediate assembly under obj, so it no longer tries to overwrite the loaded bin assembly.

The await parity point is not being ignored either. F# task state machines are structs by default, so adding or removing a let! or do! changes their layout and still fails closed on the default path. This branch now has an experimental --test:HotReloadClassStateMachines path that emits reference-type state machines when explicitly enabled. Runtime ApplyUpdate tests cover adding and removing let!, adding one inside a loop, backgroundTask, and do!. That closes the Roslyn-style behavior behind the test flag, but I have not made it the default because changing the normal task state-machine representation needs a broader compatibility and performance decision.

The refreshed umbrella head is 3c49b38. The full local verifier passes at that head: 472 service tests, 248 component tests with 2 intentional manual-host skips, both smoke modes, and direct multi-delta runtime apply.

If the original Windows reproduction still fails with the refreshed SDK branch, please let me know what you see.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

⚠️ Affects-Build-Infra Tooling check: PR touches build infrastructure ⚠️ Affects-Compiler-Output Tooling check: PR touches IL emission or codegen

Projects

Status: New

Development

Successfully merging this pull request may close these issues.

5 participants